package com.pinfly.common.crypto;

import java.io.IOException;
import java.math.BigInteger;

import com.pinfly.common.util.HexConvert;

/**
 * Static methods for working with X.509 keys. For more information regarding
 * ASN.1 and DER encoding for keys, see "A Layman's Guide to a Subset of ASN.1,
 * BER, and DER", by Burton S. Kaliski Jr.
 */
public class X509Util
{
    // Various ASN.1 types
    public static final byte SEQUENCE_TYPE = 0x30;
    public static final byte BITSTRING_TYPE = 0x03;
    public static final byte INTEGER_TYPE = 0x02;
    public static final byte OBJECT_IDENTIFIER_TYPE = 0x06;
    public static final byte NULL_TYPE = 0x05;

    // This is the complete RSA algorithm identifier
    //
    // AlgorithmIdentifier ::= SEQUENCE{
    // rsaEncryption OBJECT IDENTIFIER,
    // parameters NULL}
    //
    // Where rsaEncryption is the following:
    //
    // rsaEncryption OBJECT IDENTIFIER ::= { pkcs-1 1 }
    //
    // pkcs-1 OBJECT IDENTIFIER ::= {
    // iso(1) member-body(2) us(840) rsadsi(113549) pkcs(1) 1 }
    //
    // SEE RFC3279 for more information

    public static String RSA_ALGORITHM_IDENTIFIER = "300d06092a864886f70d0101010500";
    public static byte[] RSA_ALGORITHM_IDENTIFIER_BYTES = HexConvert.convertToBytes (RSA_ALGORITHM_IDENTIFIER);

    private X509Util ()
    {
        // Can't instantiate this class
    }

    /**
     * Creates an ASN.1 DER encoded integer.
     * 
     * @param val The integer to encode
     * @return The byte array that contains the integer converted into ASN.1 DER
     *         format.
     */
    public static byte[] createInteger (BigInteger val)
    {
        byte[] bytes = val.toByteArray ();
        byte[] length = createLength (bytes.length);

        byte[] result = KeyUtil.appendToArray (INTEGER_TYPE, null);
        result = KeyUtil.appendToArray (length, result);
        result = KeyUtil.appendToArray (bytes, result);
        return result;
    }

    /**
     * Gets an integer from the input Stream
     * 
     * @param in The stream to read from
     * @return The BigInteger
     */
    public static BigInteger readInteger (KeyInputStream in) throws IOException
    {
        // Check the type
        checkType ("INTEGER", INTEGER_TYPE, in);
        int length = readLength (in);
        return (new BigInteger (in.read (length)));
    }

    /**
     * Creates an ASN.1 DER encoded SEQUENCE
     * 
     * @param data The data to be wrapped in a sequence
     * @return The byte array that represents the sequence
     */
    public static byte[] createSequence (byte[] data)
    {
        byte[] length = createLength (data.length);
        byte[] result = KeyUtil.appendToArray (SEQUENCE_TYPE, null);
        result = KeyUtil.appendToArray (length, result);
        result = KeyUtil.appendToArray (data, result);
        return result;
    }

    /**
     * Reads a ASN.1 DER encoded SEQUENCE from a stream
     * 
     * @param in The stream to read from
     * @return The contents of the SEQUENCE
     */
    public static byte[] readSequence (KeyInputStream in) throws IOException
    {
        // Check the type
        checkType ("SEQUENCE", SEQUENCE_TYPE, in);
        int length = readLength (in);
        return (in.read (length));
    }

    /**
     * Forwards the input stream to the start of the contents of the passed in
     * ASN.1 DER encoded SEQUENCE.
     * 
     * @param in The input stream
     * @return The length of the SEQUENCE.
     */
    public static int forwardToSequenceContents (KeyInputStream in) throws IOException
    {
        // Check the type
        checkType ("SEQUENCE", SEQUENCE_TYPE, in);
        return (readLength (in));
    }

    /**
     * Creates an ASN.1 DER encoded BITSTRING
     * 
     * @param data The data to be represented as a bit string
     * @return The byte array that represents the bitstring
     */
    public static byte[] createBitString (byte[] data)
    {
        // Need to add 1 here for the unused bits
        byte[] length = createLength (data.length + 1);

        byte[] result = KeyUtil.appendToArray (BITSTRING_TYPE, null);
        result = KeyUtil.appendToArray (length, result);

        // No padded bits
        result = KeyUtil.appendToArray ((byte) 0x00, result);

        result = KeyUtil.appendToArray (data, result);
        return result;
    }

    /**
     * Forwards the input stream to the start of the contents of a ASN.1 DER
     * encoded BITSTRING.
     * 
     * @param in The stream to read from
     * @return The length of the BITSTRING contents.
     */
    public static int forwardToBitStringContents (KeyInputStream in) throws IOException
    {
        // Check the type
        checkType ("BITSTRING", BITSTRING_TYPE, in);
        int length = readLength (in);

        // Skip the padding byte
        in.readByte ();

        // Subtract the padding byte
        return (length - 1);
    }

    /**
     * Creates an RSA PKCS AlgorithmIdentifier. The structure is: <CODE><PRE>
     * AlgorithmIdentifier ::= SEQUENCE { iso(1) member-body(2) us(840)
     *   rsadsi(113549) pkcs(1) 1 1 , NULL}
     * </PRE></CODE>
     * 
     * @return The byte array that represents the RSA PKCS AlgorithmIdentifier
     */
    public static byte[] createRSAAlgorithmIdentifier ()
    {
        return (RSA_ALGORITHM_IDENTIFIER_BYTES);
    }

    /**
     * Gets an ASN.1 DER encoded AlgorithmIdentifier
     * 
     * @param in The input stream
     * @return A byte array that contains the algorithm identifier. This
     *         contains the algorithm and parameters.
     */
    public static byte[] readAlgorithmIdentifier (KeyInputStream in) throws IOException
    {
        return (readSequence (in));
    }

    /**
     * Verifies that the algorithm is RSA PKCS.
     * 
     * @param algorithmId The algorithm and parameters to verify. Should have
     *            been obtained by calling readAlgorithmIdentifier.
     * @return true if the passed in data represents a RSA PKCS algorithm,
     *         otherwise return false.
     */
    public static boolean isRSAAlgorithmIdentifier (byte[] algorithmId)
    {
        // Take the SEQUENCE header off
        String expAlg = RSA_ALGORITHM_IDENTIFIER.substring (4);
        String actualAlg = HexConvert.convertHexString (algorithmId);
        return (expAlg.equalsIgnoreCase (actualAlg));
    }

    /**
     * Creates an ASN.1 DER encoded OBJECT IDENTIFIER
     * 
     * @param data The data that represents the identifier (no header)
     * @return The byte array that represents the Object Identifier
     */
    public static byte[] createObjectIdentifier (byte[] data)
    {
        byte[] length = createLength (data.length);

        byte[] result = KeyUtil.appendToArray (OBJECT_IDENTIFIER_TYPE, null);
        result = KeyUtil.appendToArray (length, result);
        result = KeyUtil.appendToArray (data, result);
        return result;
    }

    /**
     * Reads the ASN.1 DER encoded Object Identifier from the passed in stream.
     * 
     * @param in The input stream
     * @return A byte array that contains the contents of the identifier.
     */
    public static byte[] readObjectIdentifier (KeyInputStream in) throws IOException
    {
        checkType ("OBJECT IDENTIFIER", OBJECT_IDENTIFIER_TYPE, in);
        int length = readLength (in);
        return (in.read (length));
    }

    /**
     * Creates an ASN.1 DER encoded NULL
     * 
     * @return The byte array that represents NULL
     */
    public static byte[] createNull ()
    {
        return (new byte[]
        { NULL_TYPE, 0x00 });
    }

    /**
     * Reads NULL type from the passed in stream.
     * 
     * @param in The input stream
     */
    public static void readNull (KeyInputStream in) throws IOException
    {
        checkType ("NULL", NULL_TYPE, in);
        in.readByte ();
    }

    /**
     * Creates an ASN.1 DER encoded length. <BR/>
     * 1) If length is less than 128, then the short form is used. One byte is
     * used to represent the length. Bit 8 has value "0" and bits 7-1 give the
     * length. <BR/>
     * <BR/>
     * 2) If length is greater or equal to 128, then the long form is used. Two
     * to 127 bytes are used to represent the length. Bit 8 of the first octet
     * has value "1" and bits 7-1 give the number of additional length bytes.
     * Second and following bytes give the length, base 256, most significant
     * digit first.
     * 
     * @param length The length to encode
     * @return A byte array that represents this length
     */
    public static byte[] createLength (int length)
    {
        byte[] result = null;

        if (length < 128)
        {
            // Short form - One octet.
            result = KeyUtil.appendToArray ((byte) length, null);
        }
        else
        {
            // Long form
            byte[] tmp = convertToBase256 (length);

            // Set the high bit to "1"
            // F.Y.I. - 0x80 = 10000000
            result = KeyUtil.appendToArray ((byte) (tmp.length | 0x80), null);
            result = KeyUtil.appendToArray (tmp, result);
        }

        return result;

    }

    /**
     * Reads the length header from the stream and returns the length.
     * 
     * @param in The stream to read the length from
     * @return The length
     */
    public static int readLength (KeyInputStream in) throws IOException
    {
        byte tmp = in.readByte ();

        if (tmp >> 7 == 0)
        {
            // Short format
            return tmp;

        }
        else
        {
            // Long format

            int numberOfBytesToRead = tmp & 0x7F;

            if (numberOfBytesToRead < 0)
            {
                throw new IOException ("Corrupt number of bytes to read for length.  Byte read: " + tmp);
            }

            int length = 0;

            // The values are in base 256, so we need to convert
            // them to base 10. When reading a byte, a negative number
            // indicates that the 8th bit is set to one.
            for (int i = 0; i < numberOfBytesToRead; i++)
            {
                // Need to use int here instead of byte since we will be
                // converting negative bytes to positive
                int val = in.readByte ();

                if (val < 0)
                {
                    // Need to convert this negative int into a positive
                    // by stripping off the highest bit, and adding 128
                    // F.Y.I. - 0x7F = 01111111
                    val = (val & 0x7F) + 128;
                }

                // Figure out what power of 256 we should use based on where
                // we are in the number of bytes read.
                double power = numberOfBytesToRead - 1 - i;

                length += Math.pow (256d, power) * val;
            }

            return (int) length;
        }

    }

    /**
     * Converts the passed in integer to base 256, and returns the corresponding
     * byte array. <BR/>
     * <BR/>
     * Example: <CODE><PRE>
     * input = 12345678
     * Output byte array:
     * byte[0] = 0xBC = 188
     * byte[1] = 0x61 = 97
     * byte[2] = 0x4E = 78
     * 
     * Which converts back to the input value thru the following:
     * (256^3 * 188) + (256^2 * 97) + (256^0 * 78) = 12345678
     * </PRE>
     * </CODE>
     * 
     * @param input The integer to convert
     * @return A byte array where each byte corresponds to a digit in the input
     *         integer converted to a base 256 number. The most sigificant digit
     *         resides in element 0. When interpreting each byte value, the sign
     *         bit should be interpreted as 2^7. See Example.
     * @throws IllegalArgumentException if the input integer is negative
     */
    public static byte[] convertToBase256 (int input)
    {
        // Only handles positive ints
        if (input < 0)
        {
            throw new IllegalArgumentException ("Input must be positive!");
        }

        // Get the highest power of 256 that the incoming integer
        // can be divided by.
        int power = 0;
        int y = input;
        while ((y = y >> 8) != 0)
        {
            ++power;
        }

        byte[] results = new byte[power + 1];
        int val;
        int rem = input;
        for (int i = power; i >= 0; i--)
        {
            int powValue = (int) Math.pow (256, i);
            val = rem / powValue;
            rem = rem % powValue;
            results[power - i] = new Integer (val).byteValue ();
        }

        return (results);

    }

    private static void checkType (String typeName, byte type, KeyInputStream in) throws IOException
    {
        byte readType = in.readByte ();
        if (readType != type)
        {
            throw new IOException ("Invalid key, " + typeName + " not correct!  Expected type " + type
                                   + ", but found type " + readType);
        }

    }

}
